package com.my.log.config;

import cn.hutool.json.JSONUtil;
import com.my.core.util.ClassUtil;
import com.my.core.util.JsonUtil;
import com.my.core.util.WebUtil;
import com.my.log.config.prop.BladeLogLevel;
import com.my.log.config.prop.BladeRequestLogProperties;
import io.micrometer.core.instrument.util.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.core.io.InputStreamSource;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author timo 2021/2/25
 */
@Aspect
@Configuration
@ConditionalOnWebApplication(
        type = ConditionalOnWebApplication.Type.SERVLET
)
@ConditionalOnProperty(
        value = {"blade.log.request.enabled"},
        havingValue = "true",
        matchIfMissing = true//如果没找到 默认值 相当于默认打开日志
)
@EnableConfigurationProperties(BladeRequestLogProperties.class)//加载配置对象到容器
public class RequestLogAspect {
    private static final Logger log = LoggerFactory.getLogger(RequestLogAspect.class);
    private final BladeRequestLogProperties properties;
    //R 此处日志扫描所有返回值为R的控制器
//execution(<修饰符模式>? <返回类型模式> <方法名模式>(<参数模式>) <异常模式>?)  除了返回类型模式、方法名模式和参数模式外，其它项都是可选的
//    @Around("execution(!static org.springblade.core.tool.api.R *(..)) && (@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController))")
    @Around("execution(!static com.my.core.api.R *(..)) " +
            "&& (" +
                "@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController)" +
            ")")
    public Object aroundApi(ProceedingJoinPoint point) throws Throwable {
        BladeLogLevel level = this.properties.getLevel();
        if (BladeLogLevel.NONE == level) {
            return point.proceed();
        } else {
            HttpServletRequest request = WebUtil.getRequest();
            String requestUrl = ((HttpServletRequest) Objects.requireNonNull(request)).getRequestURI();
            String requestMethod = request.getMethod();
            StringBuilder beforeReqLog = new StringBuilder(300);
            List<Object> beforeReqArgs = new ArrayList();
            beforeReqLog.append("\n\n================  Request Start  ================\n");
            beforeReqLog.append("===> {}: {}");
            beforeReqArgs.add(requestMethod);
            beforeReqArgs.add(requestUrl);
            this.logIngArgs(point, beforeReqLog, beforeReqArgs);
            this.logIngHeaders(request, level, beforeReqLog, beforeReqArgs);

            beforeReqLog.append("================    blade.log.request.enabled   ================\n");
            beforeReqLog.append("================   Request End   ================\n");
            long startNs = System.nanoTime();
            log.info(beforeReqLog.toString(), beforeReqArgs.toArray());
            StringBuilder afterReqLog = new StringBuilder(200);
            List<Object> afterReqArgs = new ArrayList();
            afterReqLog.append("\n\n================  Response Start  ================\n");
            boolean var20 = false;

            Object var13;
            try {
                var20 = true;
                Object result = point.proceed();
                if (BladeLogLevel.BODY.lte(level)) {
                    afterReqLog.append("===Result===  {}\n");
                    afterReqArgs.add(JSONUtil.toJsonStr(result));
                }

                var13 = result;
                var20 = false;
            } finally {
                if (var20) {
                    long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
                    afterReqLog.append("<=== {}: {} ({} ms)\n");
                    afterReqArgs.add(requestMethod);
                    afterReqArgs.add(requestUrl);
                    afterReqArgs.add(tookMs);
                    afterReqLog.append("================   Response End   ================\n");
                    log.info(afterReqLog.toString(), afterReqArgs.toArray());
                }
            }

            long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
            afterReqLog.append("<=== {}: {} ({} ms)\n");
            afterReqArgs.add(requestMethod);
            afterReqArgs.add(requestUrl);
            afterReqArgs.add(tookMs);
            afterReqLog.append("================   Response End   ================\n");
            log.info(afterReqLog.toString(), afterReqArgs.toArray());
            return var13;
        }
    }

    public void logIngArgs(ProceedingJoinPoint point, StringBuilder beforeReqLog, List<Object> beforeReqArgs) {
        MethodSignature ms = (MethodSignature)point.getSignature();
        Method method = ms.getMethod();
        Object[] args = point.getArgs();
        Map<String, Object> paraMap = new HashMap(16);
        Object requestBodyValue = null;

        for(int i = 0; i < args.length; ++i) {
            MethodParameter methodParam = ClassUtil.getMethodParameter(method, i);
            PathVariable pathVariable = (PathVariable)methodParam.getParameterAnnotation(PathVariable.class);
            if (pathVariable == null) {
                RequestBody requestBody = (RequestBody)methodParam.getParameterAnnotation(RequestBody.class);
                String parameterName = methodParam.getParameterName();
                Object value = args[i];
                if (requestBody != null) {
                    requestBodyValue = value;
                } else if (value instanceof HttpServletRequest) {
                    paraMap.putAll(((HttpServletRequest)value).getParameterMap());
                } else if (value instanceof WebRequest) {
                    paraMap.putAll(((WebRequest)value).getParameterMap());
                } else if (!(value instanceof HttpServletResponse)) {
                    String paraName;
                    if (value instanceof MultipartFile) {
                        MultipartFile multipartFile = (MultipartFile)value;
                        paraName = multipartFile.getName();
                        String fileName = multipartFile.getOriginalFilename();
                        paraMap.put(paraName, fileName);
                    } else {
                        if (value instanceof List) {
                            List<?> list = (List)value;
                            AtomicBoolean isSkip = new AtomicBoolean(false);
                            Iterator var17 = list.iterator();

                            while(var17.hasNext()) {
                                Object o = var17.next();
                                if ("StandardMultipartFile".equalsIgnoreCase(o.getClass().getSimpleName())) {
                                    isSkip.set(true);
                                    break;
                                }
                            }

                            if (isSkip.get()) {
                                paraMap.put(parameterName, "此参数不能序列化为json");
                                continue;
                            }
                        }

                        RequestParam requestParam = (RequestParam)methodParam.getParameterAnnotation(RequestParam.class);
                        paraName = parameterName;
                        if (requestParam != null && StringUtils.isNotBlank(requestParam.value())) {
                            paraName = requestParam.value();
                        }

                        if (value == null) {
                            paraMap.put(paraName, (Object)null);
                        } else if (ClassUtil.isPrimitiveOrWrapper(value.getClass())) {
                            paraMap.put(paraName, value);
                        } else if (value instanceof InputStream) {
                            paraMap.put(paraName, "InputStream");
                        } else if (value instanceof InputStreamSource) {
                            paraMap.put(paraName, "InputStreamSource");
                        } else if (JsonUtil.canSerialize(value)) {
                            paraMap.put(paraName, value);
                        } else {
                            paraMap.put(paraName, "此参数不能序列化为json");
                        }
                    }
                }
            }
        }

        if (paraMap.isEmpty()) {
            beforeReqLog.append("\n");
        } else {
            beforeReqLog.append(" Parameters: {}\n");
            beforeReqArgs.add(JSONUtil.toJsonStr(paraMap));
        }

        if (requestBodyValue != null) {
            beforeReqLog.append("====Body=====  {}\n");
            beforeReqArgs.add(JSONUtil.toJsonStr(requestBodyValue));
        }

    }
    //打印header
    public void logIngHeaders(HttpServletRequest request, BladeLogLevel level, StringBuilder beforeReqLog, List<Object> beforeReqArgs) {
        if (BladeLogLevel.HEADERS.lte(level)) {
            Enumeration headers = request.getHeaderNames();

            while(headers.hasMoreElements()) {
                String headerName = (String)headers.nextElement();
                String headerValue = request.getHeader(headerName);
                beforeReqLog.append("===Headers===  {}: {}\n");
                beforeReqArgs.add(headerName);
                beforeReqArgs.add(headerValue);
            }
        }

    }
    //注入
    public RequestLogAspect(final BladeRequestLogProperties properties) {
        this.properties = properties;
    }
}
